package com.ost.kautilya.modeling;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public abstract class Model implements Serializable {

	public static enum ModelOperationMode implements Serializable {
		TRAIN, PREDICT, SILENT, EVALUATION
	}

	private static final long serialVersionUID = 1L;
	public static final long DEFAULT_ID = Long.MIN_VALUE;
	public final static int PARAMETER_LIMIT = ModelParameters.COUNT + 1;

	public static enum ModelType {
		CLASSIFICATION, FORECASTING, CLUSTERING, CUSTOM
	}

	private final ModelType type;

	private transient int[] clr;
	private transient int[] atts;
	private transient String modelId;
	private transient boolean active;
	private transient Integer batch;
	private transient ModelHandler report;
	private transient String predictionTable;
	private transient Map<Integer, String> params = new TreeMap<Integer, String>();
	private transient Map<String, ModelEvaluation> evals = new Hashtable<String, ModelEvaluation>();
	private transient ModelBinary bin;
	private transient ModelMetaData meta = null;
	private String binid = null;
	private boolean pbin = false;
	private String lbname = null;
	
	public String getLastBinaryName() {
		return lbname;
	}

	public void setPbin(boolean pbin) {
		this.pbin = pbin;
	}

	public ModelBinary getBinary() {
		return bin;
	}

	public void setBinary(ModelBinary bin) {
		this.bin = bin;
	}

	public void cleanupSingletonResources() {
		if (clr != null) {
			clr = null;
		}
		if (atts != null) {
			atts = null;
		}
		if (batch != null) {
			batch = null;
		}
		if (report != null) {
			report = null;
		}
		if (evals != null) {
			evals = null;
		}
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		cleanupSingletonResources();
	}

	public ModelType getType() {
		return type;
	}

	protected Model(ModelType type) throws Exception {
		this.type = type;
		ModelBinary bin = generateBinary();
		setBinary(bin);
	}

	public void setParameter(int index, String value) {
		if (value == null) {
			params.remove(index);
		}
		if (value != null && value.trim().length() > 0)
			params.put(index, value);
	}

	public void initTransients() {
		if (params == null)
			params = new Hashtable<Integer, String>();
	}

	public String getPredictionTable() {
		return predictionTable;
	}

	public ModelHandler getHandler() {
		return report;
	}

	@Override
	public boolean equals(Object obj) {
		boolean equal = obj instanceof Model;
		if (equal) {
			Model model = (Model) obj;
			// try model id
			equal = (getModelId().equals(model.getModelId()));
			if (!equal) {
				equal = Arrays.equals(model.getAttributes(), getAttributes()) && Arrays.equals(getClasses(), model.getClasses())
						&& getHandler().getMetaIdentifier().equals(model.getHandler().getMetaIdentifier());
			}
			return equal;
		}
		return false;
	}

	public void setHandler(ModelHandler report) {
		this.report = (report);
	}

	public void setActive(boolean active) {
		this.active = active;
	}

	public String getModelId() {
		return modelId;
	}

	public void setModelId(String modelId) {
		this.modelId = modelId;
	}

	public int[] getClasses() {
		return clr;
	}

	public void setClasses(List<ReportAttribute> cls) {
		int[] clr = new int[cls.size()];
		for (int i = 0; i < cls.size(); i++)
			clr[i] = cls.get(i).getAttributeInfo().getUniqueIdentifier();
		this.clr = clr;
	}

	public void setClasses(int[] cls) {
		clr = cls;
	}

	public void setAttributes(int[] atts) {
		this.atts = (atts);
	}

	public final void prepare(ModelHandler rpt) throws Exception {
		prepare(rpt, getMode());
	}

	abstract public void applyConfiguration(ModelConfiguration config) throws Exception;

	protected String getPredictionTableName(int clz) {
		ModelHandler rpt = getHandler();
		return "PT" + rpt.getDataIdentifier() + "_" + rpt.getMetaIdentifier() + "_" + rpt.getSectionIdentifier() + "_" + clz;
	}

	abstract public Object[] predictClasses(DataRow rs) throws Exception;

	public void setAttributes(List<ReportAttribute> cls) {
		int[] atts = new int[cls.size()];
		for (int i = 0; i < cls.size(); i++)
			atts[i] = cls.get(i).getAttributeInfo().getUniqueIdentifier();
		this.atts = (atts);
	}

	public final Map<Integer, String> getParameters() {
		return params;
	}

	public boolean isActive() {
		return active;
	}

	public void setTrainingBatch(int batch) {
		this.batch = (batch);
	}

	public int getTrainingBatch() {
		return batch;
	}

	public int[] getAttributes() {
		return atts;
	}

	public abstract void trainWithRecordIncrementally(DataRow tr) throws Exception;

	public void endTraining(DataRow tr) throws Exception {
		finalizeTraining(tr);
	}

	public final void updateCycle() {
		String tnp = getParameter(ModelParameters.TRAINING_PARAMETER);
		int tp = Integer.parseInt(tnp == null ? "0" : tnp);
		int tl = Integer.parseInt(getParameter(ModelParameters.TRAINING_CYCLE));
		int tc = Integer.parseInt(getParameter(ModelParameters.TRAINING_COUNT));
		tp -= 1;
		if (tp < 0) {
			tc = -tc;
			if (tp <= tc) {
				tp = tl;
			}
		}
		setParameter(ModelParameters.TRAINING_PARAMETER, String.valueOf(tp));
	}

	abstract public void finalizeTraining(DataRow tr) throws Exception;

	public String getParameter(int i) {
		return params.get(i);
	}

	protected Object actualClassValue(DataRow sqlRs, int cli) throws SQLException {
		return sqlRs.getStringArray()[getHandler().getAttributeInfoByUID(getClasses()[cli]).getDataRowIndex()];
	}

	public abstract String getDescription();

	public abstract void processNotAccountedRow(DataRow tr) throws Exception;

	public abstract double[] getDistributionsForClasses(DataRow rs) throws Exception;

	public ModelOperationMode getMode() {
		ModelOperationMode mode = null;
		int tp = 0;
		try {
			tp = Integer.parseInt(getParameter(ModelParameters.TRAINING_PARAMETER));
		} catch (Exception e) {
			mode = ModelOperationMode.SILENT;
		} finally {
			mode = (tp > 0 ? ModelOperationMode.PREDICT : ModelOperationMode.TRAIN);
			setParameter(ModelParameters.TRAINING_PARAMETER, String.valueOf(tp));
		}
		return mode;
	}

	abstract public void evaluateRecordIncrementally(DataRow tr) throws Exception;

	abstract public void finalizeEvaluation(DataRow tr) throws Exception;

	public Map<String, ModelEvaluation> getEvaluations() {
		return evals;
	}

	public void addEvaluation(DataRow tr, ModelEvaluation en) {
		String groupId = !isBreakingAtLevel() ? getModelId() : "";
		if (isBreakingAtLevel()) {
			for (int i = 0; i < getHandler().getOrderingCount(); i++) {
				OrderingInfo oi = getHandler().getOrderingInfo(i);
				AttributeInfo info = getHandler().getAttributeInfoByUID(oi.getAttributeUID());
				int fi = info.getDataRowIndex();
				groupId += (i == 0 ? "" : " -> ") + tr.getStringArray()[fi];
			}
		}
		evals.put(groupId, en);
	}

	public String getBinaryName(Object[] groupVals) {
		String name = getBinaryName();
		if (groupVals != null) {
			for (int i = 0; i < groupVals.length; i++)
				name += "-" + String.valueOf(groupVals[i]).replaceAll("/", "|");
		}
		return name;
	}

	public String getBinaryName() {
		return binid == null ? getModelId() : binid;
	}

	public boolean canProcessRow(DataRow dr) throws Exception {
		if (getMode() == ModelOperationMode.PREDICT) {
			return (dr.isInjectedRow() && !dr.isCached()) || getHandler().hasException();
		}
		return true;
	}

	public void loadBinary(String id) throws Exception {
		if (lbname != null) {
			cleanupModelInMemory(lbname);
		}
		lbname  = id;
		ObjectInputStream in = null;
		ModelBinary bin = null;
		try {
			bin = retrieveModelInMemory(id);
			if (bin != null) {
				setBinary(bin);
				return;
			} else {
				try {
					File model = getHandler().getNamedBinaryFile(id);
					in = new ObjectInputStream(new FileInputStream(model));
					bin = (ModelBinary) in.readObject();
					setBinary(bin);
				} catch (Exception e) {
					if (id != null && !id.equals(getModelId()))
						try {
							loadBinary(binid);
						} catch (Exception e2) {
							// setBinary(generateBinary());
						}
					else {
						setBinary(generateBinary());
					}
				}
			}
		} finally {
			if (in != null)
				in.close();
			if (bin != null && !(this instanceof Forecaster))
				updateModelInMemory(id, bin);
			if (bin != null) {
//					System.out.println("Ready with model binary - " + id);
			}
		}
	}

	private final static Map<String, ModelBinary> mimc = new Hashtable<String, ModelBinary>();
	private final static Map<String, Integer> mimcCounter = new Hashtable<String, Integer>();

	private ModelBinary updateModelInMemory(String mid, ModelBinary m) {
		try {
			ModelBinary old = mimc.put(mid, m);
//			System.out.println("Loaded model binary - " + mid + " in memory.");
			if (old == null) {
				Integer counter = mimcCounter.get(mid);
				if (counter == null) {
					counter = 0;
				}
				counter += 1;
				mimcCounter.put(mid, counter);
//				System.out.println("Model binary " + mid + " user count - " + counter);
			}
			return old;
		} catch (Exception e) {
			return null;
		}
	}

	public static void cleanupModelInMemory(String mid) {
		Integer counter = mimcCounter.get(mid);
		if (counter == null) {
			counter = 1;
		}
		counter -= 1;
		if (counter == 0) {
			mimc.remove(mid);
			mimcCounter.remove(mid);
//			System.out.println("Removed model binary - " + mid + " from memory.");
		} else {
			mimcCounter.put(mid, counter);
//			System.out.println("Model binary " + mid + " user count decreased - " + counter);
		}
	}

	private ModelBinary retrieveModelInMemory(String mid) {
		ModelBinary bin = mimc.get(mid);
		if (bin != null) {
			Integer counter = mimcCounter.get(mid);
			counter += 1;
			mimcCounter.put(mid, counter);
//			System.out.println("Model binary - " + mid + " user count increased - " + counter);
		}
		return bin;
	}

	public static final String TRAINING_SET = "tins";

	public void dumpBinary(Object[] groupVals) throws Exception {
		getBinary().put(TRAINING_SET, getTrainingInstances());
		String mid = getBinaryName(groupVals);
		lbname = mid;
		File model = new File(System.getProperty("java.io.tmpdir"), mid);
		ObjectOutputStream stream = null;
		try {
			stream = new ObjectOutputStream(new FileOutputStream(model, false));
			stream.writeObject(bin);
		} finally {
			if (stream != null)
				stream.close();
		}
		getHandler().putNamedBinaryFile(model);
		if (!model.delete())
			model.deleteOnExit();
//		System.out.println("Dumped model binary - " + mid);
	}

	protected abstract Serializable getTrainingInstances();

	public void dumpBinary() throws Exception {
		dumpBinary(null);
	}

	public ModelBinary generateBinary() throws Exception {
		// System.out.println("New Binary generated !");
		return new ModelBinary();
	}

	@Override
	public String toString() {
		String meta = "[";
		meta += getType();
		return meta + "]" + getParameter(1);
	}

	abstract public void testForDataCompatability() throws Exception;

	public void setMetaData(ModelMetaData meta) {
		this.meta = meta;
		setModelId(meta.getModelId());
		setClasses(meta.getClasses());
		setAttributes(meta.getAttributes());
		for (int i = 1; i <= ModelParameters.COUNT; i++) {
			String value = meta.getParameter(i);
			if (value != null)
				setParameter(i, value);
		}
	}

	public int getReportId() {
		return meta.getReportId();
	}

	public int getRuntimeId() {
		return meta.getRuntimeId();
	}

	public int getUserId() {
		return meta.getUserId();
	}

	public ModelMetaData getMetaData() {
		return meta;
	}

	public void setBinaryId(String mid) {
		binid = mid;
	}

	public ModelConfiguration getConfiguration() {
		return getMetaData().getConfiguration();
	}

	protected boolean splitMode;
	private boolean ready = false;

	public void prepare(ModelHandler erpt, ModelOperationMode mode) throws Exception {
		try {
			setHandler(erpt);
			splitMode = getParameter(ModelParameters.TSF_GROUPING_ASSOCIATION) != null;
			// System.out.println("Applying Configuration !");
			if (getParameter(ModelParameters.APPLY_CONFIG_PREDICT_SUPPORTED) != null) applyConfiguration(getConfiguration());
		} finally {
			ready = true;
		}
	}

	@Override
	public int hashCode() {
		return getModelId().hashCode();
	}

	public boolean hasBinary() {
		return pbin;
	}

	public boolean isBreakingAtLevel() {
		return splitMode || getParameter(ModelParameters.TSF_GROUPING_ASSOCIATION) != null;
	}

	public boolean isReady() {
		return ready;
	}

	public void setReady(boolean b) {
		ready = b;
	}

	public void updateLastBinaryName(Object[] groupVals) {
		lbname = getBinaryName(groupVals);
//		System.out.println("Updated model binary name to - " + lbname);
	}
}